Skip to content

feat: set default HSTS and CSP headers for direct-exposure deployments (#14)#20

Merged
mgoldsborough merged 2 commits intomainfrom
fix/issue-14-default-hsts-csp
May 5, 2026
Merged

feat: set default HSTS and CSP headers for direct-exposure deployments (#14)#20
mgoldsborough merged 2 commits intomainfrom
fix/issue-14-default-hsts-csp

Conversation

@mgoldsborough
Copy link
Copy Markdown
Contributor

Summary

  • Previous policy of delegating HSTS/CSP to a reverse proxy left direct-exposure self-hosted deployments with no hardening. Direct-HTTPS is a supported topology, so defaults should protect it.
  • Added conservative defaults in `securityHeaders()`:
    • HSTS: `max-age=31536000; includeSubDomains` (no `preload` — that is a deliberate operator choice)
    • CSP: `default-src 'none'; frame-ancestors 'none'; base-uri 'none'` (does not affect JSON/SSE; bundle UI HTML is consumed via `fetch+srcdoc` so the response CSP does not break the iframe bridge)
  • Overrideable via `NB_HSTS` / `NB_CSP` env vars or middleware options. Env var takes precedence over option. Empty string disables the header — preserves the "delegate to proxy" behavior for operators who want it.

Test plan

  • 13 unit tests covering defaults, option override, env var override, empty-string disable, POST and 404 paths
  • `bun run lint` / `bun run check` — clean
  • README updated with new env var documentation

Closes #14

#14)

The previous stance of "HSTS/CSP belong on the reverse proxy" leaves
self-hosted OSS deployments with no hardening when no proxy sits in
front. Direct-exposure HTTPS is a supported topology, so the defaults
should protect it.

Add conservative defaults in securityHeaders():
- HSTS: max-age=31536000; includeSubDomains (no preload — that is a
  deliberate operator choice)
- CSP:  default-src 'none'; frame-ancestors 'none'; base-uri 'none'
  (no effect on JSON/SSE; bundle UI HTML is consumed via fetch+srcdoc
  so the response CSP does not break the iframe bridge, but does
  protect anyone opening that HTML directly)

Operators who terminate TLS at a proxy that already emits these
headers can disable them with NB_HSTS="" / NB_CSP="", or override to
a stricter/looser value via env var or the middleware option. Env var
takes precedence over option so ops can change the runtime policy
without a code change.
@mgoldsborough mgoldsborough force-pushed the fix/issue-14-default-hsts-csp branch from a0c3ac0 to e5c1bc8 Compare May 3, 2026 18:49
@mgoldsborough
Copy link
Copy Markdown
Contributor Author

Rebased onto current main. One adjustment from the original PR:

The proxy route in main strips upstream CSP and asserts Content-Security-Policy is null on iframed bundle responses. The PR's global default CSP default-src 'none' would block iframed bundle dev-server scripts/styles — and the test caught it.

Resolution: middleware now respects route-level intent for HSTS/CSP using the same pattern as X-Frame-Options. The proxy route opts out by setting an internal X-NB-Skip-Security-Defaults header, which the middleware strips before egress. The parent shell's frame-ancestors 'none' remains the real protection vector for iframed responses; CSP on the iframe content itself isn't the boundary that matters here.

Diff now touches src/api/routes/proxy.ts (1 import + 1 outHeaders.set) in addition to the original middleware changes. bun run verify passes.

PR #19 (the workspace generic-error PR) was also rebased — clean, no adjustments needed.

- Strip `x-nb-skip-security-defaults` from upstream proxy responses so a
  bundle dev server can't disable platform HSTS/CSP for unrelated routes.
- Add unit tests for the SKIP_DEFAULTS_HEADER opt-out path (HSTS/CSP
  skipped, header stripped from egress, other defaults still applied)
  and route-level HSTS/CSP override preservation.
- CHANGELOG entry under Changed for the new defaults + NB_HSTS/NB_CSP
  override env vars.
@mgoldsborough mgoldsborough added the qa-reviewed QA review completed with no critical issues label May 5, 2026
@mgoldsborough mgoldsborough merged commit a36666c into main May 5, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

qa-reviewed QA review completed with no critical issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add default HSTS and CSP headers for direct-exposure deployments

1 participant